package com.feedgraph;

import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedList;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.UUID;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import com.feedgraph.FeedManager.FeedItem;

import android.app.LoaderManager;


import android.content.ContentValues;
import android.content.Context;
import android.content.CursorLoader;
import android.database.Cursor;
import android.net.Uri;
import android.os.PatternMatcher;
import android.text.format.Time;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ListAdapter;

/**
 * Allows an Activity to subscribe to FeedGraph content.
 * 
 *  Use {@link FeedManager.makeFeed} if you want low-level access to feed content.
 *  
 *  Use {@link FeedManager.makeLoader} if you want to display feed content in an ListView
 *  or an Activity which supports AsyncTaskLoader.
 * 
 * If HttpResponseCache is installed, FeedManager will automatically cache responses
 */
public class FeedManager {
	
	private static final String TAG = "FeedManager";
	
  public static final Uri CONTENT_URI = Uri.parse("content://"  + FeedContentProvider.AUTHORITY + "/Feed");
  public static final String FEED_CONTENT_TYPE = "vnd.android.cursor.dir/com.bottlcap.app.feed";
  public static final String NODE = "Node";
  public static final String RELATION = "Relation";
  public static final String INDEX = "EdgeIndex";
  public static final String ID = "_id";
  public static final String JSON_OBJECT = "JSONObject";
	public static final String TIME_STAMP = "TimeStamp";
  public static final String [] PROJECTION_ALL = new String[]{ID, INDEX, NODE, RELATION, JSON_OBJECT, TIME_STAMP};

	
	public interface FeedItemAdapter {
		void bindView(Context context, View view);
		View newView(Context context, ViewGroup parent);

	}

	public static final String API_BASE_URL = "http://api.feedgraph.com/";


	protected Context mContext; 
	static private long count = 0;
	
	/**
	 * Construct a FeedManager in the onCreate method of an Activity.
	 * @param context
	 */
	public FeedManager(Context context) {
		mContext = context;
	}
	
	public Context getContext () {
		return mContext;
	}
	
	public CursorLoader makeLoader() {
		return new CursorLoader(mContext, CONTENT_URI, PROJECTION_ALL, null, null, TIME_STAMP + " DESC");
	}
	
	public void postItem(Uri uri, JSONObject json, String uuid) throws IOException {
		makeFeed(uri).postItem (json, uuid);
	}
	
	public static void postItem(String graph, String node, String rel, JSONObject json, String uuid) throws IOException {
		makeFeed(graph, node, rel).postItem(json, uuid);
	}
	
	/**
	 * Constructs a {@link Feed} object representing the content at the given URI.
	 * @param uri
	 * @return
	 */
	static public Feed makeFeed(Uri uri) {
		return new Feed(uri);
	}
	
	/**
	 * Constructs a @{link Feed} object representing content at http://api.feedgraph.com/{path}
	 * @param path
	 * @return
	 */
	static public Feed makeFeed(String graph, String node, String rel) {
		
		String path = graph + "/" + node + "/" + rel;
		
		return makeFeed(Uri.parse (API_BASE_URL + path));
	}
	
	public BasicFeed makeLocalFeed() {
		return new BasicFeed();
	}
	
	static public FeedItem parseFeedItem(JSONObject json) throws JSONException {
		return new FeedItem(json, count++);
	}
	
	public static FeedItem makeLocalFeedItem(String node, String rel, JSONObject json) {
		return FeedItem.makeLocalFeedItem (node, rel, json, count++);
	}
	
	
	public static class FeedItem implements Comparable<FeedItem> {
		
		
		protected String mUUID;
		protected int mIndex;
		protected String mNode;
		protected String mRelation;
		protected long mLocalID;
		protected JSONObject mObject;
		protected String mTimeStamp;
		protected FeedItemAdapter mAdapter;
		
		private FeedItem (JSONObject json, long localID) throws JSONException {
			mIndex = json.optInt ("Index");
			mUUID = json.optString("UUID");
			mNode = json.getString ("Node");
			mRelation = json.getString ("Relation");
			mObject = json.getJSONObject ("Object");
			mLocalID = localID;
			mTimeStamp = json.getString("TimeStamp");
		}
		
		private FeedItem() {
		}
		
		public static FeedItem makeLocalFeedItem(String node, String rel, JSONObject obj, long localID) {
			FeedItem item = new FeedItem();
			item.mUUID = UUID.randomUUID ().toString ();
			item.mNode = node;
			item.mRelation = rel;
			item.mObject = obj;
			item.mLocalID = localID;
			item.mTimeStamp = Long.toString(System.currentTimeMillis() / 1000L);
			
			return item;
		}
		
		/** can be used to compare feed items.  If two items share a key, they are guaranteed to contain the same data
		 * 
		 * @return
		 */
		public String getUUID() {
			return mUUID;
		}
		
		public long getID() {
			return mLocalID;
		}
				
		public JSONObject getObject() {
			return mObject;
		}
		
		public FeedItemAdapter getAdapter() {
			return mAdapter;
		}
		
		public void setAdapter(FeedItemAdapter adapter) {
			mAdapter = adapter;
		}

		@Override
		public int compareTo (FeedItem another) {
			return another.mIndex - this.mIndex;
		}

		public static FeedItem fromCursor (Cursor c) {
			FeedItem item = new FeedItem();
			//_id PRIMARY KEY, EdgeIndex, Node, Relation, JSONObject
			item.mUUID = c.getString (0);
			item.mIndex = c.getInt (1);
			item.mNode = c.getString (2);
			item.mRelation = c.getString(3);
			item.mTimeStamp = c.getString (5);
			try {
				item.mObject = new JSONObject(c.getString (4));
			}
			catch (JSONException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			return item;
		}
		
		public ContentValues toContentValues() {
			
			ContentValues values = new ContentValues();
			values.put (ID, mUUID);
			values.put (NODE, mNode);
			values.put (RELATION, mRelation);
			values.put (INDEX, mIndex);
			values.put (JSON_OBJECT, getObject().toString ());
			values.put (TIME_STAMP, mTimeStamp);
			return values;
		}
	}
	
	public static class BasicFeed extends ArrayList <FeedItem> {
		
		/**
		 * 
		 */
		private static final long serialVersionUID = 5735693906511076338L;

		protected BasicFeed () {
		}
		
		public int fetchNew() throws IOException, JSONException {
			return 0;
		}
	}
	
	/**
	 * Represents a stream of items which can be pushed through {@link http://FeedGraph.com}.
	 * 
	 * Each feed is stored at a URI such as {@link http://api.feedgraph.com/test/foo/bar}
	 * @author ryanne
	 *
	 */
	public static class Feed extends BasicFeed {

		/**
		 * 
		 */
		private static final long serialVersionUID = 6938189920589601828L;

		// URI to this feed
		protected Uri mUri;
		private Time version = null;
		
		private Feed(Uri uri) {
			super();
			mUri = uri;
		}

		/**
		 * Fetch the latest content of the feed, ignoring anything earlier than the given timestamp.
		 * @param since any content older than this timestamp is ignored
		 * @param waitForNewContent if true, this method will block (not return) until new content is available on the server
		 * @return
		 * @throws IOException
		 * @throws JSONException
		 */
		public int fetchSince(Time since, boolean waitForNewItems) throws IOException, JSONException {
			
			Uri.Builder builder = mUri.buildUpon ();
			
			if (since != null) {
				builder.appendQueryParameter ("since", since.format3339 (false));	
			}
			
			if (waitForNewItems) {
				builder.appendQueryParameter("block", "true");
			}
			
			Uri uri = builder.build ();
			
			Log.d(getClass().getSimpleName (), "fetching from " + uri.toString ());
			
			URLConnection conn = new URL(uri.toString ()).openConnection ();
			conn.setConnectTimeout (0);
			conn.setReadTimeout (0);
			
			String str = IOUtil.toString (conn.getInputStream ());
			
			Log.d(getClass().getSimpleName(), str);
			
			int count = 0;
			
			if (!str.equals ("null")) {

				JSONArray json = new JSONArray (str);

				for (int i = 0; i < json.length (); i++) {
					FeedItem item = FeedManager.parseFeedItem (json.getJSONObject (i));
					add (item);
				}
				
				count = json.length ();
			}
			
			if (version == null) version = new Time();
			version.setToNow ();
			
			return count;
		}
		
		/**
		 * (re-)fetches all content currently stored on the server for this feed.  Note that older content
		 * is eventually forgotten by FeedGraph.com, and that there may be no new content since the previous
		 * time this method was called.  This method may cause older {@FeedItem}s to disappear from this Feed object.
		 * @return number of new {@link FeedItem}s
		 * @throws IOException
		 * @throws JSONException
		 */
		public int fetchAll() throws IOException, JSONException {
			return fetchSince(null, false);
		}
		
		/**
		 * Fetch any new content, if it exists.
		 * @return number of new {@link FeedItem}s
		 * @throws IOException
		 * @throws JSONException
		 */
		public int fetchNew() throws IOException, JSONException {
			if (version == null) {
				return fetchAll();
			} 
			return fetchSince(version, false);
		}
		
		/**
		 * Blocks until new content is available on the server. Returns the number of new {@link FeedItem}s.
		 * @return number of new {@link FeedItem}s
		 * @throws IOException
		 * @throws JSONException
		 */
		public int waitForNewItems() throws IOException, JSONException {
			if (version == null) {
				return fetchAll();
			} 
			return fetchSince(version, true);
		}
		
		/**
		 * Publish a JSON object to this feed, causing a new FeedItem to be created on the server.
		 * @param json
		 * @throws MalformedURLException
		 * @throws IOException
		 */
		public void postItem(JSONObject json, String uuid) throws IOException {
			
			Uri.Builder builder = mUri.buildUpon ();
			if (uuid != null) builder.appendQueryParameter ("uuid", uuid);
			Uri uri = builder.build ();
			
			Log.d(getClass().getSimpleName (), "posting to " + uri.toString ());
			Log.d(getClass().getSimpleName (), json.toString ());
			
			byte [] bytes = json.toString().getBytes ();
			
			HttpURLConnection conn = (HttpURLConnection) new URL(uri.toString ()).openConnection ();
			conn.setDoOutput(true);
      conn.setRequestProperty("Content-Type", "application/json");
      conn.setRequestMethod("POST");    

			OutputStream out = conn.getOutputStream ();
			out.write (bytes);
			out.flush ();
			out.close ();
 
			if (conn.getResponseCode () != 200) {
				throw new IOException(conn.getResponseMessage ());
			}
		}

	}
	
	public static String makeUUID() {
		return UUID.randomUUID ().toString ();
	}
	
	public interface FeedListener {
		void onFeedItem(FeedItem item);
	}
	
}
